using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace Viewer
{
    using SWF = System.Windows.Forms;
    using System.IO;

    using CameraType = ArcBallCamera;

    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        Model model;
        CameraType cam;
        SpriteBatch spriteBatch;
        SpriteFont spriteFont;

        Matrix world = Matrix.Identity;

        float nearClip = 1f;
        float farClip = 100f;

        bool rotateModel = false;

        Matrix[] boneTransforms;
        Vector3 modelCenter;
        float modelRadius;
        int primitiveCount;
        int vertexCount;

        bool doLighting = true;

        KeyboardState kbd, lastKbd;

        SWF.OpenFileDialog dlg = new SWF.OpenFileDialog() { Filter = "XNA Models (*.xnb)|*.xnb|All files (*.*)|*.*" };

        static int CountStringOccurrences(string text, string pattern)
        {
            // Loop through all instances of the string 'text'.
            int count = 0;
            int i = 0;
            while ((i = text.IndexOf(pattern, i)) != -1)
            {
                i += pattern.Length;
                count++;
            }
            return count;
        }

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            this.IsMouseVisible = true;
            this.Window.AllowUserResizing = true;

            cam = new CameraType(this);
            this.Components.Add(cam);

            this.Window.ClientSizeChanged += new EventHandler<EventArgs>(Window_ClientSizeChanged);

            kbd = Keyboard.GetState();
        }

        void Window_ClientSizeChanged(object sender, EventArgs e)
        {
            cam.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f),
                this.GraphicsDevice.Viewport.AspectRatio, nearClip, farClip);
        }

        protected override void Initialize()
        {
            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            spriteFont = Content.Load<SpriteFont>("MyFont");
            
            model = Content.Load<Model>("Collada");
            UpdateModel("Collada");

            base.LoadContent();
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            if (spriteBatch != null)
            {
                spriteBatch.Dispose();
                spriteBatch = null;
            }

            base.UnloadContent();
        }

        /// <summary>
        /// Whenever a new model is selected, we examine it to see how big
        /// it is and where it is centered. This lets us automatically zoom
        /// the display, so we can correctly handle models of any scale.
        /// </summary>
        void MeasureModel()
        {
            // Look up the absolute bone transforms for this model.
            boneTransforms = new Matrix[model.Bones.Count];

            model.CopyAbsoluteBoneTransformsTo(boneTransforms);

            // Compute an (approximate) model center position by
            // averaging the center of each mesh bounding sphere.
            modelCenter = Vector3.Zero;

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = boneTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                modelCenter += meshCenter;
            }

            modelCenter /= model.Meshes.Count;

            // Now we know the center point, we can compute the model radius
            // by examining the radius of each mesh bounding sphere.
            modelRadius = 0;

            foreach (ModelMesh mesh in model.Meshes)
            {
                BoundingSphere meshBounds = mesh.BoundingSphere;
                Matrix transform = boneTransforms[mesh.ParentBone.Index];
                Vector3 meshCenter = Vector3.Transform(meshBounds.Center, transform);

                float transformScale = transform.Forward.Length();

                float meshRadius = (meshCenter - modelCenter).Length() +
                                   (meshBounds.Radius * transformScale);

                modelRadius = Math.Max(modelRadius, meshRadius);
            }

            nearClip = MathHelper.Clamp(modelRadius / 100f, 0.1f, 1000f);
            farClip = MathHelper.Max(modelRadius * 100, nearClip + modelRadius + 1f);

        }

        void ModelStats()
        {
            primitiveCount = vertexCount = 0;
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    primitiveCount += part.PrimitiveCount;
                    vertexCount += part.NumVertices;
                }
            }
        }

        void UpdateLighting()
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    if (doLighting)
                    {
                        effect.EnableDefaultLighting();
                        effect.PreferPerPixelLighting = true;
                    }
                    else
                    {
                        effect.LightingEnabled = false;
                        effect.PreferPerPixelLighting = false;
                    }
                }
            }
        }

        void UpdateModel(string fileName)
        {
            // update fields
            MeasureModel();
            ModelStats();

            UpdateLighting();

            //
            cam.MouseRate.Z = 0.05f * modelRadius; // zoom rate

            cam.zoomMin = nearClip;
            cam.zoomMax = farClip;

            cam.LookAt(modelCenter);
            cam.Zoom = 3 * modelRadius;

            cam.Home();

            cam.projectionMatrix = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45f),
                this.GraphicsDevice.Viewport.AspectRatio, nearClip, farClip);

            //world = Matrix.CreateTranslation(modelCenter);

            modelInfoStr = String.Format("Model - {0} (#V={1}, #F={2}, Radius={3})",
                               Path.GetFileNameWithoutExtension(fileName), vertexCount, primitiveCount, modelRadius);
        }

        void LoadModel(string fileName)
        {
            switch (Path.GetExtension(fileName.ToLower()))
            {
                case ".xnb":
                    model = Content.Load<Model>(Path.GetFileNameWithoutExtension(fileName));
                    break;
                default:
                    throw new ArgumentException("Unsupported file: " + fileName);
            }

            UpdateModel(fileName);
        }

        void LoadModel()
        {
            // Show open file dialog box
            if (dlg.ShowDialog() != SWF.DialogResult.OK)
                return;

            LoadModel(dlg.FileName);
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            lastKbd = kbd;
            kbd = Keyboard.GetState();

            // Allows the game to exit
            if (kbd.IsKeyDown(Keys.Escape))
                this.Exit();

            if (kbd.IsKeyDown(Keys.L) && lastKbd.IsKeyUp(Keys.L))
            {
                if (kbd.IsKeyDown(Keys.LeftControl))
                    LoadModel();
                else
                {
                    doLighting = !doLighting;
                    UpdateLighting();
                }
            }

            if (kbd.IsKeyDown(Keys.R) && lastKbd.IsKeyUp(Keys.R))
            {
                rotateModel = !rotateModel;
            }

            if (kbd.IsKeyDown(Keys.I) && lastKbd.IsKeyUp(Keys.I))
            {
                DoDrawInfo = !DoDrawInfo;
            }

            if (kbd.IsKeyDown(Keys.Home) && lastKbd.IsKeyUp(Keys.Home))
            {
                cam.Home();
            }

            if (rotateModel)
                cam.HorizontalAngle = (float)gameTime.TotalGameTime.TotalSeconds;

            base.Update(gameTime);
        }

        void DrawModel()
        {
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.World = boneTransforms[mesh.ParentBone.Index] * world;
                    effect.View = cam.viewMatrix;
                    effect.Projection = cam.projectionMatrix;
                }
                mesh.Draw();
            }
        }

        static bool DoDrawInfo = true;
        string modelInfoStr = String.Empty;
        void DrawInfo()
        {
            spriteBatch.Begin();
            spriteBatch.DrawString(spriteFont, modelInfoStr, 8 * Vector2.One, Color.White);
            spriteBatch.End();

            // restore states to default
            this.GraphicsDevice.DepthStencilState = DepthStencilState.Default;
            this.GraphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;
            this.GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // Draw the model.
            if (model != null)
            {
                DrawModel();

                if (DoDrawInfo)
                    DrawInfo();
            }

            base.Draw(gameTime);
        }
    }
}
